/*
 * ******************************************************************************
 * Class: ServiceManager	
 * 
 * @author: Teresa Mafalda dos Reis Frazao
 * @version:April 10, 2014 
 * 
 * This class instantiates all data structures using Singleton class, and runs 
 * all graph algorithms: Topological Sort and BFS. Topological Sort is used to 
 * identify services upfront, and BFS is used to start and stop all services that 
 * are at the sale graph level.  
 * ******************************************************************************
 */
package com.feedzai.service;

import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;
import java.util.Queue;
import java.util.Stack;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;

import com.feedzai.service.data.ServiceNode;
import com.feedzai.service.exception.CyclicDependenciesException;
import com.feedzai.service.exception.ServiceMalfunctionException;
import com.feedzai.service.utils.ServiceManagerHelper;
import com.feedzai.service.utils.Singleton;
import com.feedzai.service.worker.ServiceManagerWorker;

/**
 * The Class ServiceManager.
 */
public class ServiceManager {


	/** The service manager helper. */
	private static ServiceManagerHelper serviceManagerHelper;
	
	/**  Keep the order of visited nodes. */
	private static Stack<Long> visitedNodes; 

	/**
	 * The main method.
	 *
	 * @param args the arguments
	 */
	public static void main(String[] args) {
		
		// Lets initialize out structures
		Singleton.getInstance();
		
		// Now we can create out helper
		serviceManagerHelper = new ServiceManagerHelper();
		
		visitedNodes = new Stack<Long>();
		
		try {
			createAlfaServices();
		} catch (ServiceMalfunctionException e) {
			// The service that uses this interface shall decide what to do with this exception
			e.printStackTrace();
		}
	}
	
	
	/**
	 * Sets the service manager helper.
	 *
	 * @param serviceManagerHelper the new service manager helper
	 */
	public static void setServiceManagerHelper(
			ServiceManagerHelper serviceManagerHelper) {
		ServiceManager.serviceManagerHelper = serviceManagerHelper;
	}
	
	/**
	 * Sets the visited nodes.
	 *
	 * @param visitedNodes the new visited nodes
	 */
	public static void setVisitedNodes(Stack<Long> visitedNodes) {
		ServiceManager.visitedNodes = visitedNodes;
	}

	/**
	 * Gets the visited nodes.
	 *
	 * @return the visited nodes
	 */
	public static Stack<Long> getVisitedNodes() {
		return visitedNodes;
	}
	
	/**
	 * Gets the service manager helper.
	 *
	 * @return the service manager helper
	 */
	public static ServiceManagerHelper getServiceManagerHelper() {
		return serviceManagerHelper;
	}
	
	/*
	 * ---------------------------------------------------------------------------
	 * 							Private Interface 
	 * ---------------------------------------------------------------------------
	 */
	
	
	/**
	 * Creates the alfa services.
	 *
	 * @throws ServiceMalfunctionException the service malfunction exception
	 */
	public static void createAlfaServices() throws ServiceMalfunctionException
	{
		ServiceNode serviceNode0 = serviceManagerHelper.createService(null);
		ServiceNode serviceNode1 = serviceManagerHelper.createService(null);
		ServiceNode serviceNode2 = serviceManagerHelper.createService(null);
		ServiceNode serviceNode3 = serviceManagerHelper.createService(null);
		ServiceNode serviceNode4 = serviceManagerHelper.createService(null);
		ServiceNode serviceNode5 = serviceManagerHelper.createService(null);
		ServiceNode serviceNode6 = serviceManagerHelper.createService(null);
		ServiceNode serviceNode7 = serviceManagerHelper.createService(null);
		
		// With cycle
//		serviceManagerHelper.addServiceDependency(serviceNode0.getId(), serviceNode1.getId());
//		serviceManagerHelper.addServiceDependency(serviceNode0.getId(), serviceNode2.getId());
//		serviceManagerHelper.addServiceDependency(serviceNode1.getId(), serviceNode3.getId());
//		serviceManagerHelper.addServiceDependency(serviceNode3.getId(), serviceNode2.getId());
//		serviceManagerHelper.addServiceDependency(serviceNode4.getId(), serviceNode3.getId());
//		serviceManagerHelper.addServiceDependency(serviceNode2.getId(), serviceNode4.getId());
			
		
	
		//Without cycle
		serviceManagerHelper.addServiceDependency(serviceNode2.getId(), serviceNode0.getId());
		
		serviceManagerHelper.addServiceDependency(serviceNode2.getId(), serviceNode1.getId());
		serviceManagerHelper.addServiceDependency(serviceNode5.getId(), serviceNode1.getId());
		serviceManagerHelper.addServiceDependency(serviceNode6.getId(), serviceNode1.getId());
		
		serviceManagerHelper.addServiceDependency(serviceNode6.getId(), serviceNode2.getId());
		
		serviceManagerHelper.addServiceDependency(serviceNode5.getId(), serviceNode3.getId());
		
		serviceManagerHelper.addServiceDependency(serviceNode6.getId(), serviceNode4.getId());
	
		serviceManagerHelper.addServiceDependency(serviceNode6.getId(), serviceNode5.getId());
		
		try {
			topologicalSort();

			startServicesBFS();
			
//			stopServicesBFS();
			
			dummyStopService();
		} catch (CyclicDependenciesException e) {
			throw new ServiceMalfunctionException("Services cannot run since there are Cyclic Dependencies.");
		}
	}
	
	
	/**
	 * Topological sort.
	 * 
	 * <p>This algorithm algorithm identifies cyclic dependencies by removing all edges from 
	 * child services tp parent services. If dependencies exist, then there is a cycle. 
	 *
	 * @return the linked list
	 * @throws CyclicDependenciesException the cyclic dependencies exception
	 */
	public static LinkedList<ServiceNode> topologicalSort() throws CyclicDependenciesException
	{
		// Sorted services list
		LinkedList<ServiceNode> sortedServices = new LinkedList<ServiceNode>();
		
		//
		// All services before sort. We need to check if sorted list is equivalent to this list.
		// if not we have a cyclic dependency
		//
		LinkedList<ServiceNode> allServices = new LinkedList<ServiceNode>();
		allServices.addAll(Singleton.getDependencyListUtils().getAvailableServices());
		
		// Get nodes which have no parent dependencies
		LinkedList<ServiceNode> parentNodes = serviceManagerHelper.getParentNodes();
		
		// If there are not any services whe have a cyclic dependency
		if(parentNodes == null || parentNodes.isEmpty())
			throw new CyclicDependenciesException("Unable to start service since there are Cyclic Dependencies");
		
		
		while(!parentNodes.isEmpty())
		{
			ServiceNode parentNode = parentNodes.remove();
			sortedServices.add(parentNode);
			
			LinkedList<Long> out = parentNode.getIsParentFromDependencies();
					
			for(int i=0; i<out.size(); i++)
			{
				ServiceNode descendent = Singleton.getDependencyListUtils().getAServiceNode(out.get(i));
				
				for(int j=0; j<descendent.getIsChildFromDependencies().size(); j++)
				{
					// Remove edges from child services
					if(descendent.getIsChildFromDependencies().get(j).longValue() == parentNode.getId().longValue()) 
					{
						descendent.getIsChildFromDependencies().remove(j);
						
						// Stop the for
						j = descendent.getIsChildFromDependencies().size();
					}
				}
				
				// If there are no dependencies we can add the node to parent nodes 
				if(descendent.getIsChildFromDependencies().isEmpty())
					parentNodes.add(descendent);
			}
		}
		
		// To check if there are cyclic dependencies check if sortedServices is equivalent to allServices list. 
	    if(sortedServices.size() < allServices.size()){
	    	throw new CyclicDependenciesException("Unable to start service since there are Cyclic Dependencies");
	    }else{
	    	return sortedServices;
	    }	
	}
	
	
	/**
	 * Since we can have multiple parent nodes, we need to run BFS on all of them and check if they were visited or not.
	 *
	 * @return operation result
	 * @throws CyclicDependenciesException the cyclic dependencies exception
	 */
	public static boolean startServicesBFS() throws CyclicDependenciesException 
	{

		LinkedList<ServiceNode> servicesList = serviceManagerHelper.getParentNodes();
		
		if(servicesList== null|| servicesList.size()<=0)
		{
			return false;
		}
		
		visitedNodes.clear();

		for(int i=0; i<servicesList.size(); i++)
		{
			if(visitedNodes.search(servicesList.get(i).getId()) == -1)
			{
				runBfsInARoot(servicesList.get(i), false, true);
			}
		}
		
		// Time to clear visited nodes
		serviceManagerHelper.unmarkAllServices();
		
		return true;
	}
	
	
	/**
	 * Stop services BFS.
	 * Like <code>startServicesBFS</code> we need to run all leaves services (services that have no child nodes.
	 *
	 * @throws CyclicDependenciesException the cyclic dependencies exception
	 */
	public static void stopServicesBFS() throws CyclicDependenciesException 
	{
		LinkedList<ServiceNode> servicesList = serviceManagerHelper.getChildNodes();
		
		for(int i=0; i<servicesList.size(); i++)
		{
			runBfsInARoot(servicesList.get(i), true, false);
		}
		
		// Time to clear visited nodes
		serviceManagerHelper.unmarkAllServices();
		visitedNodes.clear();
	}
	
	/**
	 * Dummy stop service.
	 * TODO remove this method and use stopServicesBFS when working properly
	 */
	public static void dummyStopService()
	{

		while (!visitedNodes.isEmpty()) 
		{
			ServiceNode serviceNode = Singleton.getDependencyListUtils().getAServiceNode(visitedNodes.pop());
			
			//
			// Set visited to false, so that we can view the node.
			//
			serviceManagerHelper.stopService(serviceNode);
		} 
	}
	
	
	/**
	 * Start services using Breadth First Search algorithm.
	 * Although this algorithm has worst performance than DFS it guarantees a solution.
	 * Additionally we should start for the top of the three and travel until its leafs. 
	 * 
	 * When we visit a node we need:
	 * <p>1. Check if the node has dependencies.
	 * <p><p>1.1. If it has dependencies, check if they already started or stooped based on <code>start</code> value.
	 * <p><p><p>1.1.1. If they started then start the current service
	 * <p><p><p>1.1.2. Otherwise do not start the service
	 *
	 * @param rootNode the root node
	 * @param viewIsChildFrom the view is child from or is parent from
	 * @param start the start or stop service switch
	 * @throws CyclicDependenciesException when there are no nodes without any dependencies
	 */
	public static void runBfsInARoot(ServiceNode rootNode, boolean viewIsChildFrom, boolean start) throws CyclicDependenciesException
	{
		Queue<ServiceNode> queue = new LinkedList<ServiceNode>();
		
		// There are no parent nodes so we return an exception
		//
		if(rootNode == null)
			throw new CyclicDependenciesException("Unable to start service since there are not any root nodes");
		
		queue.add(rootNode);
		serviceManagerHelper.markService(rootNode);
		
		// 
		// We are able to start or stop the service.
		//
		if(start)
		{
			if(visitedNodes.search(rootNode.getId()) == -1)
			{
				serviceManagerHelper.startService(rootNode);
				visitedNodes.push(rootNode.getId());
			}	
		}
		else
		{
			serviceManagerHelper.stopService(rootNode);
		}
		
		

		while (!queue.isEmpty()) 
		{
			ServiceNode serviceNode = queue.remove();
			ServiceNode childNode = null;

			// Get unvisited child nodes
			Integer poolSize = serviceManagerHelper.getAllUnvisitedNodes(serviceNode, viewIsChildFrom).size();
			
			// Start multiple ExecutorService to process service updates simultaniously
			ExecutorService service = null;
			List<Future<Runnable>> futures = null;
			
			if(poolSize!=null && poolSize > 0) {
				service = Executors.newFixedThreadPool(poolSize);
			    futures = new ArrayList<Future<Runnable>>();
			}
			else
			{
				//FIXME remove static number of thread poll elements. 
				service = Executors.newFixedThreadPool(10);
			    futures = new ArrayList<Future<Runnable>>();
			}
			
			while ((childNode=serviceManagerHelper.getUnvisitedNode(serviceNode, viewIsChildFrom))!= null) 
			{

				//
				// Start worker threads that will start or stop a service.
				//
				
				if(visitedNodes.search(childNode.getId()) == -1)
				{
				
					ServiceManagerWorker childWorker = new ServiceManagerWorker(childNode, serviceManagerHelper, start);
					Future futureChildNode = service.submit(childWorker);
					futures.add(futureChildNode);
	
					serviceManagerHelper.markService(childNode);
					visitedNodes.push(childNode.getId());
					queue.add(childNode);
				}
			}
			
			// wait for all tasks to complete before continuing
		      for (Future<Runnable> futuresRunnable : futures)
		      {
		         try {
		        	 futuresRunnable.get();
				} catch (InterruptedException | ExecutionException e) {
					// TODO Auto-generated catch block
					e.printStackTrace();
				}
		      }
		}
	}
}
